package main

// 客户端程序实现了UDP打洞的客户端部分
// 通过与中转服务器和另一个客户端的交互，实现NAT穿透并建立P2P直接连接

import (
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
	"time"
)

// 当前客户端的标识符，用于在日志中区分不同的客户端
var tag string

// 用于UDP打洞握手的固定消息
const HAND_SHAKE_MSG = "我是打洞消息"

func main() {
	// 从命令行参数获取当前进程标记字符串，便于在日志中区分不同客户端
	tag = os.Args[1]

	// 设置本地UDP地址，使用任意IP(0.0.0.0)和固定端口9982
	// 注意：端口必须固定，这样NAT设备才能正确映射和转发数据包
	srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9982}

	// 设置中转服务器的地址和端口
	// 这里使用的是公网IP地址，所有客户端都能访问
	dstAddr := &net.UDPAddr{IP: net.ParseIP("101.200.121.40"), Port: 9981}

	// 创建UDP连接，连接到中转服务器
	conn, err := net.DialUDP("udp", srcAddr, dstAddr)
	if err != nil {
		fmt.Println(err)
	}

	// 向中转服务器发送标识信息，表明自己是新的客户端
	if _, err = conn.Write([]byte("hello, I'm new peer:" + tag)); err != nil {
		log.Panic(err)
	}

	// 创建缓冲区，用于接收中转服务器返回的另一个客户端的地址信息
	data := make([]byte, 1024)

	// 从UDP连接中读取数据
	// 中转服务器会返回另一个客户端的公网地址和端口
	n, remoteAddr, err := conn.ReadFromUDP(data)
	if err != nil {
		fmt.Printf("error during read: %s", err)
	}

	// 关闭与中转服务器的连接，因为已经获取到了另一个客户端的地址信息
	conn.Close()

	// 解析另一个客户端的地址信息
	anotherPeer := parseAddr(string(data[:n]))

	// 打印本地地址、服务器地址和另一个客户端的地址信息
	fmt.Printf("local:%s server:%s another:%s\n", srcAddr, remoteAddr, anotherPeer.String())

	// 开始UDP打洞过程，尝试与另一个客户端建立直接连接
	bidirectionHole(srcAddr, &anotherPeer)
}
func parseAddr(addr string) net.UDPAddr {
	t := strings.Split(addr, ":")
	port, _ := strconv.Atoi(t[1])
	return net.UDPAddr{
		IP:   net.ParseIP(t[0]),
		Port: port,
	}
}
func bidirectionHole(srcAddr *net.UDPAddr, anotherAddr *net.UDPAddr) {
	conn, err := net.DialUDP("udp", srcAddr, anotherAddr)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	// 向另一个peer发送一条udp消息(对方peer的nat设备会丢弃该消息,非法来源),用意是在自身的nat设备打开一条可进入的通道,这样对方peer就可以发过来udp消息
	if _, err = conn.Write([]byte(HAND_SHAKE_MSG)); err != nil {
		log.Println("send handshake:", err)
	}
	go func() {
		for {
			time.Sleep(10 * time.Second)
			if _, err = conn.Write([]byte("from [" + tag + "]")); err != nil {
				log.Println("send msg fail", err)
			}
		}
	}()
	for {
		data := make([]byte, 1024)
		n, _, err := conn.ReadFromUDP(data)
		if err != nil {
			log.Printf("error during read: %s\n", err)
		} else {
			log.Printf("收到数据:%s\n", data[:n])
		}
	}
}
